﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Collections.Specialized;
using System.Web.Mvc;
using System.Text.RegularExpressions;
using System.IO;
using System.Web;
using System.Runtime.Caching;
using Microsoft.Scripting.Hosting;
using Ruandao.HostServer;
using System.Xml;

namespace Ruandao.MiniFace
{

    public delegate string MapPathFunction(string url);
    public delegate string GetUrlBaseFunction();

    public class Template
    {
        public static MapPathFunction MapPath; 
        public static GetUrlBaseFunction GetUrlBase;

        public string TemplateUrlBase { get; set; }  // 模板的基准路径
        public string FilePath { get; set; }         // 相对于模板基准的模板文件的路径

        private List<Content> pageContents;          // 模板的内存对象模型

        public Template(string filePath)
        {
            string urlBase = "~/Templates/Default/";
            if (Template.GetUrlBase != null)
                urlBase = Template.GetUrlBase();

            Create(urlBase, filePath);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="controllerContext"></param>
        /// <param name="templateUrlBase">模板文件所在位置的基准路径。</param>
        /// <param name="filePath">模板文件的相对路径。</param>
        public Template(string templateUrlBase, string filePath)
        {
            Create(templateUrlBase, filePath);
        }

        public void Create(string templateUrlBase, string filePath)
        {
            this.FilePath = filePath;
            this.TemplateUrlBase = templateUrlBase.EndsWith("/") ? templateUrlBase : templateUrlBase + "/";

            string url = Ruandao.Misc.UtilityFunction.CombineUrl(this.TemplateUrlBase, this.FilePath);
            string file = GetPhysicalPath(url);
            ObjectCache cache = MemoryCache.Default;
            pageContents = cache["TEMPLATE_CONTENTS_" + file] as List<Content>;

            if (pageContents == null)
            {
                CacheItemPolicy policy = new CacheItemPolicy();
                List<string> filePaths = new List<string>();
                filePaths.Add(file);

                policy.ChangeMonitors.Add(new HostFileChangeMonitor(filePaths));

                StringBuilder html = parseInclude(filePath);// 解析include
                
                html = MergeLayoutFile(html.ToString());// 解析layout

                // 替换模板路径和应用程序路径的占位符。
                html.Replace("{~}/", VirtualPathUtility.ToAbsolute("~/"));
                html.Replace("{^}/", VirtualPathUtility.ToAbsolute(this.TemplateUrlBase));

                // 相对于内容文件所在路径的资文件的路径替换。注意：相对于include文件和layout文件自身的引用资源已经在此之前被替换了。
                string currentPath = url.Substring(0, url.LastIndexOf("/") + 1);
                html.Replace("{.}/", VirtualPathUtility.ToAbsolute(currentPath));

                pageContents = ParseContent(html.ToString());// 解析模板文件.

                cache.Set("TEMPLATE_CONTENTS_" + filePath, pageContents, policy);
            }

        }

        private StringBuilder MergeLayoutFile(string contentFileHtml)
        {
            StringBuilder layoutHtml = parseLayout(ref contentFileHtml);

            if ( layoutHtml == null )  // 没有使用Layout指令
            {
                return new StringBuilder (contentFileHtml); // 递归出口。
            }

            string pattern = "(<@content\\s+name=\"(?<name>.*)\"\\s*>(?<middle>([\\s\\S]*))</content@>)";
            string removepattern = "<@content\\s+name=\".*\"\\s*>[\\s\\S]+</content@>";
            MatchCollection contentsmatch = Regex.Matches(contentFileHtml, pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline);
            
            Dictionary<string, string> contents = new Dictionary<string, string>();
            
            string body = Regex.Replace(contentFileHtml, removepattern, ""); //content之后的内容，默认为body
            contents.Add("body",body);
            for (int i = 0; i < contentsmatch.Count; i++)
            {
                //Groups的下标1为：innertext,Groups的下标4为：属性名为name的值
                string middle = contentsmatch[i].Groups[1].Value;
                string name = contentsmatch[i].Groups[4].Value;
                contents.Add(name, middle);
            }

            string html = parsePlace(layoutHtml.ToString(), contents);
            return MergeLayoutFile(html);   // 递归解析模板文件锁使用的Layout布局模板文件。
        }

        /// <summary>
        /// 匹配content标签
        /// </summary>
        /// <param name="layoutHtml">layout文件的内容。</param>
        /// <param name="contents">content标签，key为name,value为内容</param>
        /// <returns>替换后的content标签中间的内容</returns>
        public string parsePlace(string layoutHtml,Dictionary<string,string> contents)
        {
            string placePattern = "(<@place\\s+name=\"(?<name>.*)\"\\s*>(?<middle>([\\s\\S]*))</place@>)";
            MatchCollection placeMiddles = Regex.Matches(layoutHtml,
                                                           placePattern,
                                                           RegexOptions.IgnoreCase |
                                                           RegexOptions.Multiline |
                                                           RegexOptions.Singleline);
            for (int i = 0; i < placeMiddles.Count; i++)
            {
                if (contents.ContainsKey(placeMiddles[i].Groups["name"].Value))
                    layoutHtml = Regex.Replace(layoutHtml,
                                                placeMiddles[i].Value,
                                                contents[placeMiddles[i].Groups["name"].Value]);
            }
            return layoutHtml;
        }

        /// <summary>
        /// 读取模板文件，递归展开include所包含的文本。
        /// </summary>
        /// <param name="filename">相对于模板基准路径的文件相对路径。</param>
        /// <returns>展开后的文档。</returns>
        private StringBuilder parseInclude(string filename)
        {
            StringBuilder html = readTemplateFile(filename);
            string pattern = "<@include([ ]+)src=\"(?<file>.*)\"([ ]+)/@>";
            MatchCollection matches = Regex.Matches(html.ToString(), pattern);
            foreach (Match match in matches)
            {
                string file = match.Groups["file"].Value;
                html.Replace(match.Value, parseInclude(file).ToString());  //递归调用
            }

            return html;
        }

        /// <summary>
        /// 读出layout标签所指向的布局文件的内容，包含其中用include指令嵌入的文本，同时用引用参数返回移除了layout指令的文本。
        /// </summary>
        /// <param name="html">文档内容，该内容中可能包含layout指令。</param>
        /// <returns>返回布局文件的内容，如果没有使用布局文件则返回null。</returns>
        private StringBuilder parseLayout(ref string contentFileHtml)
        {
            string layoutpattern = "<@layout\\s+src=\"(?<file>.*)\"\\s*/@>";
            
            Match match = Regex.Match(contentFileHtml, layoutpattern);
            string file = match.Groups["file"].Value; //找到layout文件

            contentFileHtml = Regex.Replace(contentFileHtml, layoutpattern, "");
            if (string.IsNullOrWhiteSpace(file))
                return null;

            return parseInclude(file);  // 读取布局文件的同时展开其中的嵌入文件。
        }

        /// <summary>
        /// 读取文档内容, 同时解析其中的"{.}/"占位符（相对于文档本身的相对路径）。
        /// </summary>
        /// <param name="file">相对于模板基准路径的文件相对路径。</param>
        /// <returns>文档内容。</returns>
        private StringBuilder readTemplateFile(string file)
        {
            string url = Ruandao.Misc.UtilityFunction.CombineUrl(this.TemplateUrlBase, file);
            file = GetPhysicalPath(url);
            StringBuilder html = new StringBuilder();
            using (StreamReader reader = new StreamReader(file))
            {
                html.Append(reader.ReadToEnd());
            }

            string currentPath = url.Substring(0,url.LastIndexOf("/") + 1);
            html.Replace("{.}/", VirtualPathUtility.ToAbsolute( currentPath ) );
            return html;
        }



        public static string GetPhysicalPath(string url)
        {
            string file;
            if (Template.MapPath == null)
            {
                file = HttpContext.Current.Server.MapPath(url);
            }
            else
            {
                file = Template.MapPath(url);
            }
            return file;
        }
        
        public void Render(TemplateContext templateContext, TextWriter writer)
        {
            foreach (Content content in pageContents)
            {
                content.Render( templateContext, writer);
            }
        }

        private List<Content> ParseContent(string html)
        {
            List<Content> contents = new List<Content>();

            string pattern = @"\{\*.*?\*\}|\{=.*?=\}|\{%.*?%\}|<\$.*?\$>|<@.*?@>";
            foreach (var segment in new Traversal(html, pattern))
            {
                if (!segment.IsTarget)
                {
                    contents.Add(new TextItem(this, segment.Text));
                    continue;
                }
                string prefix = segment.Text.Substring(0, 2);
                string middle = segment.Text.Substring(2, segment.Text.Length - 4);
                switch (prefix)
                {
                    case "{*":
                        contents.Add(new CommentText(this,middle));
                        break;
                    case "{=":
                        contents.Add(new ExpressionItem(this, middle));
                        break;
                    case "{%":
                        contents.Add(new ScriptBlock(this, middle));
                        break;
                    case "<$":
                        contents.Add(new WidgetElement(this, "<" + middle + ">"));
                        break;
                    case "<@":
                        contents.Add(new Directive(this, "<" + middle + ">"));
                        break;
                }

            }
            return contents;
        }

    }
}
